package org.hzz.promotions.domain.service.impl;

import io.gitee.pkmer.convention.exception.AppCommonException;
import io.gitee.pkmer.convention.exception.request.BadRequestException;
import io.gitee.pkmer.convention.page.PageResponse;
import io.gitee.pkmer.convention.page.query.PageQuery;
import io.gitee.pkmer.convention.service.BaseDomainService;
import io.gitee.pkmer.core.base32.Base32Plus;
import io.gitee.pkmer.core.base32.SignBase32;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Setter;
import org.hzz.promotions.domain.entity.CouponEntity;
import org.hzz.promotions.domain.entity.ExchangeCodeEntity;
import org.hzz.promotions.domain.repository.ExchangeCodeRepository;
import org.hzz.promotions.domain.service.CouponDomainService;
import org.hzz.promotions.domain.service.ExchangeCodeDomainService;
import org.hzz.promotions.types.constants.RedisKeys;
import org.hzz.promotions.types.enums.code.CodeStatus;
import org.hzz.promotions.types.enums.code.CodeType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;

import static org.hzz.config.constants.RedisConstants.Promotions.COUPON_CODE_SERIAL_KEY;

/**
 * @author <a href="mailto:1193094618@qq.com">pkmer</a>
 * <br>
 * <a href = "https://gitee.com/developeros/videos-online">Code Repository</a>
 * At 2024/8/31
 */
@Service
public class ExchangeCodeDomainServiceImpl
        extends BaseDomainService<ExchangeCodeRepository>
        implements ExchangeCodeDomainService {

    /**
     * Base32转码表
     */
    private final String ALPHABET_TABLE = "RO2BKFC4ZHESQ37GJPLAT5DNYVI6MWUX";

    /**
     * 填充字符 到10个
     */
    private final Character PADDING = '8';

    /**
     * 大质数随机种子
     */
    private final Long SEED = 0b1011_0101_0101_1001L;

    @Setter(onMethod_ = @Autowired)
    private StringRedisTemplate redisTemplate;

    @Autowired
    @Lazy
    private CouponDomainService couponDomainService;

    private ThreadPoolTaskExecutor taskExecutor;

    /**
     * 签名加密
     */
    private SignBase32 signBase32;

    @PostConstruct
    public void init() {
        CompletableFuture.runAsync(() -> {
            logger.info("初始化线程池和Base32");
            initThreadPool();
            initBase32();
        });
    }


    private void initThreadPool() {
        // CPU密集型
        final int corePoolSize = Runtime.getRuntime().availableProcessors();

        taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(corePoolSize); // 设置核心线程数
        taskExecutor.setMaxPoolSize(corePoolSize); // 设置最大线程数
        taskExecutor.setQueueCapacity(20); // 设置队列容量
        taskExecutor.setThreadNamePrefix("code-"); // 设置线程名前缀
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true); // 设置所有任务完成前关闭
        // 设置拒绝策略
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        taskExecutor.initialize(); // 初始化线程池
    }

    private void initBase32() {
        Base32Plus base32Plus = Base32Plus.builder()
                .setEncodeTable(ALPHABET_TABLE)
                .setCharNum(10)
                .setPadding(PADDING)
                .get();
        signBase32 = new SignBase32(base32Plus, SEED);
    }

    @PreDestroy
    public void destory() {
        logger.info("关闭线程池");
        this.taskExecutor.shutdown();
    }


    /**
     * {@inheritDoc}
     *
     * @param targetId 兑换码的目标id
     * @param totalNum 要生成的数量
     * @param endTime  有效截止时间
     */
    @Override
    public void asyncGenerateCode(Long targetId, int totalNum, LocalDateTime endTime) {

        int maxSerialNum = redisTemplate.boundValueOps(COUPON_CODE_SERIAL_KEY).increment(totalNum).intValue();

        List<ExchangeCodeEntity> entities = new ArrayList<>(totalNum);
        for (int i = maxSerialNum - totalNum + 1; i <= maxSerialNum; i++) {
            String code = signBase32.generateCode(i);
            logger.info("优惠券兑换码-> {}", code);

            ExchangeCodeEntity exchangeCodeEntity = new ExchangeCodeEntity()
                    .setId(i)
                    .setCode(code)
                    .setExchangeTargetId(targetId)
                    .setExpiredTime(endTime)
                    .setType(CodeType.COUPON);
            entities.add(exchangeCodeEntity);
        }

        repository.batchInsertExchangeCodes(entities);
    }

    /**
     * {@inheritDoc}
     *
     * @param targetId 目标id
     */
    @Override
    public void deleteCouponCodeByTargetId(Long targetId) {
        repository.deleteCouponCodeByTargetId(targetId);
    }

    @Override
    public PageResponse<ExchangeCodeEntity> pageQuery(PageQuery pageQuery) {
        return repository.pageQueryCode(pageQuery);
    }

    /**
     * {@inheritDoc}
     * @param code 兑换码
     * @param type 兑换码类型
     * @return
     */
    @Override
    public ExchangeCodeEntity getExchangeCodeEntityByCode(String code,CodeType type) {
        int serialNum = signBase32.getSerialNum(code);
        if (checkCodeIsUsed(serialNum)) {
            throw new AppCommonException(String.format("该兑换码(%s)已经被使用过", code));
        }

        return repository.selectBySerialNum(serialNum,type)
                .orElseThrow(() -> new BadRequestException("兑换码不存在"));
    }

    /**
     * {@inheritDoc}
     * @param serialNum 序列号 其实就是id
     * @param userId 用戶id
     */
    @Override
    public void useExchangeCode(int serialNum,long userId) {
        ExchangeCodeEntity updateEntity = new ExchangeCodeEntity()
                .setId(serialNum)
                .setUserId(userId)
                .setStatus(CodeStatus.EXCHANGED);

        repository.updateExchangeCode(updateEntity);
        updateCodeMark(serialNum, true);
    }

    /**
     * 设置兑换码领取状态
     *
     * @param serialNum 兑换码序列号
     * @param mark      true为已领取，false为未领取
     */
    private Boolean updateCodeMark(long serialNum, boolean mark) {
        // 如果不存在插入一条记录 返回false,如果已经存在再出入一条同样的记录返回true
        return redisTemplate.opsForValue().setBit(
                RedisKeys.COUPON_CODE_MARK,
                serialNum,
                mark);
    }

    /**
     * 检查兑换码是否已被使用
     *
     * @param serialNum 兑换码序列号
     * @return true 已经使用 false 未使用
     */
    private boolean checkCodeIsUsed(long serialNum) {
        Boolean bit = redisTemplate.opsForValue().getBit(
                RedisKeys.COUPON_CODE_MARK,
                serialNum
        );

        return bit != null && bit;
    }

}
